unit uakeebajson;

{$mode objfpc}{$H+}

interface

uses
  superobject, Classes;

const
  // Encapsulation constants
  AKEEBA_JSON_ENCAPSULATION_RAW = 1; // Raw JSON data
  AKEEBA_JSON_ENCAPSULATION_AESCTR128 = 2; // AES-128 in CTR (counter) mode
  AKEEBA_JSON_ENCAPSULATION_AESCTR256 = 3; // AES-256 in CTR (counter) mode
  AKEEBA_JSON_ENCAPSULATION_AESCBC128 = 4; // AES-128 in CBC mode - preferred
  AKEEBA_JSON_ENCAPSULATION_AESCBC256 = 5; // AES-256 in CBC mode

  // JSON Statuses
  AKEEBA_JSON_STATUS_OK      = 200; // OK
  AKEEBA_JSON_STATUS_NOTAUTH = 401; // Not authorized (invalid credentials)
  AKEEBA_JSON_STATUS_NOPRIV  = 403; // Not enough privileges
  AKEEBA_JSON_STATUS_NOTFOUND = 404; // Resource not found
  AKEEBA_JSON_STATUS_NOMETHOD = 405; // No valid method specified
  AKEEBA_JSON_STATUS_ERROR   = 500; // An error occurred
  AKEEBA_JSON_STATUS_NOTIMPLEMENTED = 501; // Method not implemented
  AKEEBA_JSON_STATUS_INACTIVE = 503; // JSON service is not activated
  AKEEBA_JSON_STATUS_INVALIDDATA = 600; // Non-JSON data received

type
  TAkeebaProfileRecord = record
    id:   integer;
    Name: string;
  end;
  TAkeebaProfilesArray = array of TAkeebaProfileRecord;

  TAkeebaFilenameRecord = record
    part: integer;
    Name: string;
    size: int64;
  end;
  TAkeebaFilenamesArray = array of TAkeebaFilenameRecord;

  TAkeebaBackupsRecord = record
    id:      integer;
    description: string;
    comment: string;
    backupstart: TDateTime;
    backupend: TDateTime;
    status:  string;
    origin:  string;
    backupType: string;
    profileId: string;
    archiveName: string;
    absolutePath: string;
    multipart: integer;
    tag:     string;
    filesexist: integer;
    meta:    string;
    size:    int64;
    filenames: TAkeebaFilenamesArray;
  end;
  TAkeebaBackupsArray = array of TAkeebaBackupsRecord;

  // JSON Response record
  TAkeebaResponse = record
  	Encapsulation: Byte;	// Encapsulation of the response (used in d/l)
    Status: integer;      	// Status code
    Msg:    string;			// Error message if Status > 200 or raw JSON-encoded Data if Status = 200
  end;

  TAkeebaReponseAPI = record
    Status: integer;
    ErrorMessage: string;
    api:    integer;
    component: string;
    date:   string;
  end;

  TAkeebaResponseProfiles = record
    Status:   integer;
    ErrorMessage: string;
    profiles: TAkeebaProfilesArray;
  end;

  TAkeebaResponseBackup = record
    Status:   integer;
    ErrorMessage: string;
    HasRun:   boolean;
    Domain:   string;
    Step:     string;
    Substep:  string;
    Error:    string;
    Warnings: TStringList;
  end;

  TAkeebaReponseBackups = record
    Status: integer;
    ErrorMessage: string;
    BackupRecords: TAkeebaBackupsArray;
  end;

  TAkeebaResponseBackupInfo = record
    Status: integer;
    ErrorMessage: string;
    BackupRecord: TAkeebaBackupsRecord;
  end;

  TAkeebaResponseDownload = record
    Status:   integer;
    ErrorMessage: string;
    FileData: string;
  end;

  TAkeebaRequestBackup = record
    profile:     integer;
    description: string;
    comment:     string;
  end;

  TAkeebaRequestBackups = record
    from:  integer;
    limit: integer;
  end;

  TAkeebaAutosniffStatus = (NotFound, NotEnabled, InvalidLogin, MiscError, NoEncryption, Encryption128, Encryption256);

  { TAkeebaJSON }

  TAkeebaJSON = class
  private
    FEncapsulation: byte;
    FURL:      string;
    FPassword: string;
    FKey:      string;

    // Proxy setup
    FProxyName:      string;
    FProxyPort:      integer;
    FProxyUserName:  string;
    FProxyPassword:  string;
    FProxyBasicAuth: boolean;

    procedure setEncapsulation(aValue: byte);
    function makeChallenge(): string;
    function makeKey: string;
    function HTTPEncode(const AStr: string): string;
    function StrToDateTime(s: string): TDateTime;
  public
    // Simple object constructor
    constructor Create();

    // Low-level function
    function runMethod(method: string; Data: ISuperObject): TAkeebaResponse;

    // Public API
    function autoSniff(SiteURL: string): TAkeebaAutosniffStatus;
    function getVersion: TAkeebaReponseAPI;
    function getProfiles: TAkeebaResponseProfiles;
    function startBackup: TAkeebaResponseBackup; overload;
    function startBackup(req: TAkeebaRequestBackup): TAkeebaResponseBackup; overload;
    function stepBackup: TAkeebaResponseBackup;
    function listBackups(): TAkeebaReponseBackups; overload;
    function listBackups(req: TAkeebaRequestBackups): TAkeebaReponseBackups;
      overload;
    function getBackupInfo(backupId: integer): TAkeebaResponseBackupInfo;
    function download(backupId, part, segment: integer): TAkeebaResponseDownload;
    function Delete(backupId: integer): boolean;
    function deleteFiles(backupId: integer): boolean;
    function getLog(tag: string): string;

    // Public properties
    property Encapsulation: byte Read FEncapsulation Write setEncapsulation;
    property URL: string Read FURL Write FURL;
    property Password: string Read FPassword Write FPassword;

    // Proxy setup
    property ProxyName: string Read FProxyName Write FProxyName;
    property ProxyPort: integer Read FProxyPort Write FProxyPort;
    property ProxyUserName: string Read FProxyUserName Write FProxyUserName;
    property ProxyPassword: string Read FProxyPassword Write FProxyPassword;
    property ProxyBasicAuth: boolean Read FProxyBasicAuth Write FProxyBasicAuth;
  end;

implementation

uses
  StrUtils, SysUtils, Variants,
  md5, akaesctr, httpsend;

const
  B64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

function Base64Encode(const S: string): string;
var
  InBuf:  array[0..2] of byte;
  OutBuf: array[0..3] of char;
  iI, iJ: integer;
begin
  SetLength(Result, ((Length(S) + 2) div 3) * 4);
  for iI := 1 to ((Length(S) + 2) div 3) do
  begin
    if Length(S) < (iI * 3) then
      Move(S[(iI - 1) * 3 + 1], InBuf, Length(S) - (iI - 1) * 3)
    else
      Move(S[(iI - 1) * 3 + 1], InBuf, 3);
    OutBuf[0] := B64Table[((InBuf[0] and $FC) shr 2) + 1];
    OutBuf[1] := B64Table[(((InBuf[0] and $3) shl 4) or ((InBuf[1] and $F0) shr 4)) + 1];
    OutBuf[2] := B64Table[(((InBuf[1] and $F) shl 2) or ((InBuf[2] and $C0) shr 6)) + 1];
    OutBuf[3] := B64Table[(InBuf[2] and $3F) + 1];
    Move(OutBuf, Result[(iI - 1) * 4 + 1], 4);
  end;
  if Length(S) mod 3 = 1 then
  begin
    Result[Length(Result) - 1] := '=';
    Result[Length(Result)]     := '=';
  end
  else if Length(S) mod 3 = 2 then
    Result[Length(Result)] := '=';
end;

function Base64Decode(const S: string): string;
var
  OutBuf: array[0..2] of byte;
  InBuf:  array[0..3] of byte;
  iI, iJ: integer;
begin
  if Length(S) mod 4 <> 0 then
    raise Exception.Create('Base64: Incorrect string format');
  SetLength(Result, ((Length(S) div 4) - 1) * 3);
  for iI := 1 to (Length(S) div 4) - 1 do
  begin
    Move(S[(iI - 1) * 4 + 1], InBuf, 4);
    for iJ := 0 to 3 do
      case InBuf[iJ] of
        43: InBuf[iJ] := 62;
        48..57: Inc(InBuf[iJ], 4);
        65..90: Dec(InBuf[iJ], 65);
        97..122: Dec(InBuf[iJ], 71);
        else
          InBuf[iJ] := 63;
      end;
    OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
    OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
    OutBuf[2] := (InBuf[2] shl 6) or (InBuf[3] and $3F);
    Move(OutBuf, Result[(iI - 1) * 3 + 1], 3);
  end;
  if Length(S) <> 0 then
  begin
    Move(S[Length(S) - 3], InBuf, 4);
    if InBuf[2] = 61 then
    begin
      for iJ := 0 to 1 do
        case InBuf[iJ] of
          43: InBuf[iJ] := 62;
          48..57: Inc(InBuf[iJ], 4);
          65..90: Dec(InBuf[iJ], 65);
          97..122: Dec(InBuf[iJ], 71);
          else
            InBuf[iJ] := 63;
        end;
      OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
      Result    := Result + char(OutBuf[0]);
    end
    else if InBuf[3] = 61 then
    begin
      for iJ := 0 to 2 do
        case InBuf[iJ] of
          43: InBuf[iJ] := 62;
          48..57: Inc(InBuf[iJ], 4);
          65..90: Dec(InBuf[iJ], 65);
          97..122: Dec(InBuf[iJ], 71);
          else
            InBuf[iJ] := 63;
        end;
      OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
      OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
      Result    := Result + char(OutBuf[0]) + char(OutBuf[1]);
    end
    else
    begin
      for iJ := 0 to 3 do
        case InBuf[iJ] of
          43: InBuf[iJ] := 62;
          48..57: Inc(InBuf[iJ], 4);
          65..90: Dec(InBuf[iJ], 65);
          97..122: Dec(InBuf[iJ], 71);
          else
            InBuf[iJ] := 63;
        end;
      OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
      OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
      OutBuf[2] := (InBuf[2] shl 6) or (InBuf[3] and $3F);
      Result    := Result + char(OutBuf[0]) + char(OutBuf[1]) + char(OutBuf[2]);
    end;
  end;
end;

{ TAkeebaJSON }

function TAkeebaJSON.autoSniff(SiteURL: string): TAkeebaAutosniffStatus;
var
	bTrySSL		: boolean;
	ar			: TAkeebaReponseAPI;
	indexPos	: Integer;
begin
	// This function will allow us to automatically determine the correct site URL
	// to use and whether we should enable encrypted communications

	// First attempt is to try the Site URL given, without encryption, if a ? is found
	indexPos := Pos('?', SiteURL);
	if(indexPos > 0) then
	begin
    	URL := SiteURL;
        Encapsulation := AKEEBA_JSON_ENCAPSULATION_RAW;
        ar := getVersion();
    end
    else
    	ar.Status := AKEEBA_JSON_STATUS_ERROR;

    // If we couldn't get in, let's see if the URL is messed up
    if(ar.Status <> AKEEBA_JSON_STATUS_OK) then
    begin
    	// Does the thing contain index.php in it? If so, strip it
    	indexPos := Pos('index.php', SiteURL);
    	if(indexPos > 0) then SiteURL := LeftStr(SiteURL, indexPos - 1);
    	// Append a / at the end, if necessary
    	if( RightStr(SiteURL, 1) <> '/' ) then SiteURL := SiteURL + '/';
    	// Append the full URL to the JSON API
    	SiteURL := SiteURL + 'index.php?option=com_akeeba&format=raw&view=json';
    	// If there's no http:// or https:// defined, prefix SiteURL with http://
    	if( (LowerCase(LeftStr(SiteURL, 8)) <> 'https://') and (LowerCase(LeftStr(SiteURL, 7)) <> 'http://') ) then
    		SiteURL := 'http://' + SiteURL;
    	URL := SiteURL;
    	ar := getVersion();
    end;

    // Catch authentication error
    if(ar.Status <> AKEEBA_JSON_STATUS_OK) then
    begin
    	if(ar.Status = AKEEBA_JSON_STATUS_INACTIVE) then
    		Result := NotEnabled
    	else if(ar.Status = AKEEBA_JSON_STATUS_INVALIDDATA) then
    		Result := NotFound
    	else if( (ar.Status = AKEEBA_JSON_STATUS_NOTAUTH) or (ar.Status = AKEEBA_JSON_STATUS_NOPRIV) ) then
    		Result := InvalidLogin
    	else
    		Result := MiscError;
    	Exit();
    end;

    // Let's try AES-128 with 256-bit CBC encryption
    Encapsulation := AKEEBA_JSON_ENCAPSULATION_AESCBC256;
    ar := getVersion();
    if(ar.Status = AKEEBA_JSON_STATUS_OK) then
    begin
    	Result := Encryption256;
    	Exit;
    end;

    // Let's try AES-128 with 128-bit CBC encryption
    Encapsulation := AKEEBA_JSON_ENCAPSULATION_AESCBC128;
    ar := getVersion();
    if(ar.Status = AKEEBA_JSON_STATUS_OK) then
    begin
    	Result := Encryption128;
    	Exit;
    end;

    Encapsulation := AKEEBA_JSON_ENCAPSULATION_RAW;
    Result := NoEncryption;
end;

constructor TAkeebaJSON.Create();
begin
  inherited;
  FEncapsulation := AKEEBA_JSON_ENCAPSULATION_RAW;
end;

function TAkeebaJSON.Delete(backupId: integer): boolean;
begin
  // @TODO - Implement me
end;

function TAkeebaJSON.deleteFiles(backupId: integer): boolean;
begin
  // @TODO - Implement me
end;

function TAkeebaJSON.download(backupId, part, segment: integer): TAkeebaResponseDownload;
var
	dataObject,
	respObject		: ISuperObject;
	Response		: TAkeebaResponse;
	aProfile		: TAkeebaProfileRecord;
	i				: Integer;
begin
	dataObject := SO('{}');
	dataObject.AsObject.I['backup_id'] := backupId;
	dataObject.AsObject.I['part_id'] := part;
	dataObject.AsObject.I['segment'] := segment;
	Response := runMethod('download', dataObject);

	Result.Status := Response.Status;
    if(Response.Status = 200) then
    begin
        Result.ErrorMessage := '';
        if(Response.Encapsulation = AKEEBA_JSON_ENCAPSULATION_RAW) then
        	Result.FileData := Base64Decode(Response.Msg)
        else
        	Result.FileData := Response.Msg;
    end
    else
    begin
        Result.ErrorMessage := Response.Msg;
    end;

end;

function TAkeebaJSON.getBackupInfo(backupId: integer): TAkeebaResponseBackupInfo;
var
	dataObject,
	respObject			: ISuperObject;
	filesList			: TSuperArray;
	Response			: TAkeebaResponse;
	aBackup				: TAkeebaBackupsRecord;
	i					: Integer;
begin
	dataObject := SO('{}');
	dataObject.AsObject.I['backup_id'] := backupId;
	Response := runMethod('getBackupInfo', dataObject);

	Result.Status := Response.Status;
	if(Response.Status = 200) then
	begin
		respObject := SO(Response.Msg);
		Result.ErrorMessage := '';
		with respObject.AsObject do
		begin
			aBackup.id				:= I['id'];
			aBackup.description		:= S['description'];
			aBackup.comment			:= S['comment'];
			aBackup.backupstart		:= StrToDateTime(S['backupstart']);
			aBackup.backupend		:= StrToDateTime(S['backupend']);
			aBackup.status			:= S['status'];
			aBackup.origin			:= S['origin'];
			aBackup.backupType		:= S['type'];
			aBackup.profileId		:= S['profile_id'];
			aBackup.archiveName		:= S['archivename'];
			aBackup.absolutePath	:= S['absolute_path'];
			aBackup.multipart		:= I['multipart'];
			aBackup.tag				:= S['tag'];
			aBackup.filesexist		:= I['filesexist'];
			aBackup.meta			:= S['meta'];
			aBackup.size			:= I['size'];
		end;
		if(respObject.asObject['filenames'].IsType(stArray)) then
		begin
			filesList := respObject.asObject['filenames'].AsArray();
			SetLength(aBackup.filenames, filesList.Length);
			for i := 0 to filesList.Length - 1 do
			begin
				aBackup.filenames[i].part := filesList[i].AsObject.I['part'];
				aBackup.filenames[i].Name := filesList[i].AsObject.S['name'];
				aBackup.filenames[i].size := filesList[i].AsObject.I['size'];
			end;
		end
		else
			SetLength(aBackup.filenames, 0);
		Result.BackupRecord := aBackup;
    end
    else
    begin
    	Result.ErrorMessage := Response.Msg;
    end;
end;

function TAkeebaJSON.getLog(tag: string): string;
begin
  // @TODO - Implement me
end;

function TAkeebaJSON.getProfiles: TAkeebaResponseProfiles;
var
	dataObject,
	respObject		: ISuperObject;
	Response		: TAkeebaResponse;
	aProfile		: TAkeebaProfileRecord;
	j				: Integer;
begin
	dataObject := SO('{}');
	Response := runMethod('getProfiles', dataObject);

	Result.Status := Response.Status;
	if(Response.Status = 200) then
	begin
		respObject := SO(Response.Msg);
        Result.ErrorMessage := '';
		if(respObject.IsType(stArray)) then
		begin
        	SetLength(Result.Profiles, respObject.AsArray.Length);
        	for j := 0 to respObject.AsArray.Length - 1 do
        		with respObject.AsArray[j].AsObject do
        		begin
        			Result.profiles[j].id := I['id'];
        			Result.profiles[j].name := S['name'];
        		end;
		end;
	end
	else
		Result.ErrorMessage := Response.Msg;
	begin
	end;
end;

function TAkeebaJSON.getVersion: TAkeebaReponseAPI;
var
	dataObject		: ISuperObject;
	Response		: TAkeebaResponse;
begin
	dataObject := SO('{}');
	Response := runMethod('getVersion', dataObject);

	Result.Status := Response.Status;
	if(Response.Status = 200) then
	begin
		dataObject := SO(Response.Msg);
		Result.ErrorMessage := '';
		Result.api			:= dataObject.AsObject.I['api'];
		Result.component	:= dataObject.AsObject.S['component'];
		Result.date			:= dataObject.AsObject.S['date'];
	end
	else
	begin
		Result.ErrorMessage := Response.Msg;
	end;
end;

function TAkeebaJSON.HTTPEncode(const AStr: string): string;
const
  NoConversion = ['A'..'Z', 'a'..'z', '*', '@', '.', '_', '-'];
var
  Sp, Rp: PChar;
begin
  SetLength(Result, Length(AStr) * 3);
  Sp := PChar(AStr);
  Rp := PChar(Result);
  while Sp^ <> #0 do
  begin
    if Sp^ in NoConversion then
      Rp^ := Sp^
    else if Sp^ = ' ' then
      Rp^ := '+'
    else
    begin
      FormatBuf(Rp^, 3, '%%%.2x', 6, [Ord(Sp^)]);
      Inc(Rp, 2);
    end;
    Inc(Rp);
    Inc(Sp);
  end;
  SetLength(Result, Rp - PChar(Result));
end;

function TAkeebaJSON.listBackups(): TAkeebaReponseBackups;
var
	req				: TAkeebaRequestBackups;
begin
	req.from := 0;
    req.limit := 50;
    Result := listBackups(req);
end;

function TAkeebaJSON.listBackups(req: TAkeebaRequestBackups): TAkeebaReponseBackups;
var
	dataObject		: ISuperObject;
	respObject		: TSuperArray;
	Response		: TAkeebaResponse;
	aBackup			: TAkeebaBackupsRecord;
	i				: Integer;
begin
	// Sanitize input
    if(req.from < 0) then req.from := 0;
    if(req.limit <= 0) then req.limit := 50;

    dataObject := SO('{}');
    dataObject.AsObject.I['from'] := req.from;
    dataObject.AsObject.I['limit'] := req.limit;
    Response := runMethod('listBackups', dataObject);

    Result.Status := Response.Status;
    if(Response.Status = 200) then
    begin
    	respObject := SO(Response.Msg).AsArray;
    	Result.ErrorMessage := '';
    	SetLength(Result.BackupRecords, respObject.Length);
    	if( respObject.Length > 0 ) then
	    	for I := 0 to respObject.Length - 1 do
	    	begin
	    		with respObject[i] do
	    		begin
        			aBackup.id				:= I['id'];
        			aBackup.description		:= S['description'];
        			aBackup.comment			:= S['comment'];
        			aBackup.backupstart		:= StrToDateTime(S['backupstart']);
        			aBackup.backupend		:= StrToDateTime(S['backupend']);
        			aBackup.status			:= S['status'];
        			aBackup.origin			:= S['origin'];
        			aBackup.backupType		:= S['type'];
        			aBackup.profileId		:= S['profile_id'];
        			aBackup.archiveName		:= S['archivename'];
        			aBackup.absolutePath	:= S['absolute_path'];
        			aBackup.multipart		:= I['multipart'];
        			aBackup.tag				:= S['tag'];
        			aBackup.filesexist		:= I['filesexist'];
        			aBackup.meta			:= S['meta'];
        			aBackup.size			:= I['size'];
	    		end;
	    		Result.BackupRecords[i] := aBackup;
	    	end;
    end
    else
    begin
    	Result.ErrorMessage := Response.Msg;
    end;
end;

function TAkeebaJSON.makeChallenge(): string;
var
  challenge: WideString;
  i:    integer;
  salt: WideString;
  md5:  WideString;
  md5interim: TMDDigest;
begin
  // Create the secure key
  salt := makeKey;
  salt := 'foobar';
  challenge := salt + FPassword;
  md5interim := MDString(challenge, MD_VERSION_5);
  md5    := LowerCase(MDPrint(md5interim));
  Result := salt + ':' + md5;
end;

function TAkeebaJSON.makeKey: string;
const
  Chars: WideString = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
var
  i:    integer;
  salt: WideString;
begin
  Randomize;
  salt := '';
  for i := 1 to 64 do
  begin
    salt := salt + Chars[Random(Length(Chars)) + 1];
  end;
  Result := salt;
end;

function TAkeebaJSON.runMethod(method: string; Data: ISuperObject): TAkeebaResponse;
var
	myurl,
	datastr,
	bodyjson,
	requestjson,
	json			: string;
    abResObj,
    abResBody,
	abReqBody,
	abReqObj		: ISuperObject;
	HTTP			: THTTPSend;
	ss				: TStringStream;
	firstHash,
	lastHash		: Integer;
begin
	// Create the request body object
	datastr := Data.AsJSon();
	abReqBody := SO('{"method":"'+method+'", "data": '+datastr+'}');

	// Add a challenge or a response key, depending on encapsulation
	if(Self.FEncapsulation = AKEEBA_JSON_ENCAPSULATION_RAW) then
		abReqBody.AsObject.s['challenge'] := makeChallenge()
	else
	begin
		FKey := makeKey();
		abReqBody.AsObject.s['key'] := FKey;
	end;

	// Get the JSON representation of the request body
	bodyjson := abReqBody.AsJSon();

	if(Encapsulation > AKEEBA_JSON_ENCAPSULATION_RAW) then
	begin
		AESSetPassword(FPassword);
		case Encapsulation of
			AKEEBA_JSON_ENCAPSULATION_AESCBC128:
				bodyjson := AESEncryptCBC(bodyjson);
			AKEEBA_JSON_ENCAPSULATION_AESCBC256:
				bodyjson := AESEncryptCBC(bodyjson);
			AKEEBA_JSON_ENCAPSULATION_AESCTR128:
				raise Exception.Create('CTR mode is not supported');
			AKEEBA_JSON_ENCAPSULATION_AESCTR256:
				raise Exception.Create('CTR mode is not supported');
		end;
	end;

	// Create and encode the request object
	abReqObj := SO('{"encapsulation": "'+IntToStr(encapsulation)+'", "body": ""}');
	abReqObj.AsObject.S['body'] := bodyjson;
	requestjson := abReqObj.AsJSon();

	// Create the GET URL
	myurl := FURL + '&json=' + HTTPEncode(requestjson);

	// Run the web request
    HTTP := THTTPSend.Create;
    if self.ProxyName <> '' then
    begin
    	HTTP.ProxyHost := ProxyName;
    	HTTP.ProxyPort := IntToStr(ProxyPort);
    	if(ProxyBasicAuth) then
    	begin
        	HTTP.ProxyUser := ProxyUserName;
        	HTTP.ProxyPass := ProxyPassword;
    	end;
    end;
    if not HTTP.HTTPMethod('GET', myurl) then
    begin
    	result.Status := AKEEBA_JSON_STATUS_ERROR;
    	result.Msg := 'HTTP status ' + IntToStr(Http.Resultcode) + ' received.';
    end;
    ss := TStringStream.Create('');
    ss.CopyFrom(HTTP.Document, HTTP.Document.Size);
    ss.Seek(0, soFromBeginning);
    json := ss.ReadString(ss.Size);
    ss.Free;
    HTTP.Free;

	// Check for invalid data
    if( Trim(json) = '' ) then
    begin
        Result.Status := AKEEBA_JSON_STATUS_INVALIDDATA;
        Result.Msg := 'Invalid JSON data received'; // No translate
        Exit;
    end;

    // Find the first and last occurence of ###
    firstHash := Pos('###', json);
    lastHash := LastDelimiter('#', json);
    if ((lastHash - firstHash) < 5) or (firstHash = 0) then
    begin
    	// JSON data not found in the response?!
        Result.Status := AKEEBA_JSON_STATUS_INVALIDDATA;
        Result.Msg := 'JSON data not found in response'; // No Translate
        Exit;
    end;

    if(firstHash < lastHash) then
    	json := MidStr(json, firstHash + 3, lastHash - firstHash - 5)
    else
    	json := '{}';

    abResObj := SO(json);
	abResBody := abResObj.AsObject['body'];
	Result.Status := abResBody.AsObject.I['status'];
	Result.Msg := abResBody.AsObject.S['data'];
	Result.Encapsulation := abResObj.AsObject.I['encapsulation'];

	// Decode Result.Msg if other encapsulation is selected
	case(Result.Encapsulation) of
		AKEEBA_JSON_ENCAPSULATION_AESCTR128:
			raise Exception.Create('CTR mode is not supported');
		AKEEBA_JSON_ENCAPSULATION_AESCTR256:
			raise Exception.Create('CTR mode is not supported');
		AKEEBA_JSON_ENCAPSULATION_AESCBC128:
			begin
			AESSetPassword(FKey);
			Result.Msg := AESDecryptCBC(Result.Msg);
			end;
		AKEEBA_JSON_ENCAPSULATION_AESCBC256:
			begin
			AESSetPassword(FKey);
			Result.Msg := AESDecryptCBC(Result.Msg);
			end;
	end;
end;

procedure TAkeebaJSON.setEncapsulation(aValue: byte);
begin
	FEncapsulation := aValue;
end;

function TAkeebaJSON.startBackup(req: TAkeebaRequestBackup): TAkeebaResponseBackup;
var
	dataObject		: ISuperObject;
	respObject		: ISuperObject;
	warnObject		: TSuperArray;
	Response		: TAkeebaResponse;
	aProfile		: TAkeebaProfileRecord;
	i				: Integer;
begin
	// Sanitize input
    if(req.profile < 1) then req.profile := 1;
    if(req.description = '') then req.description := 'Default description'; // @TODO - Translate
    if(req.comment = '') then req.comment := 'Default comment'; // @TODO - Translate

    dataObject := SO('{}');
    dataObject.AsObject.I['profile'] := req.profile;
    dataObject.AsObject.S['description'] := req.description;
    dataObject.AsObject.S['comment'] := req.comment;
    Response := runMethod('startBackup', dataObject);

    Result.Status := Response.Status;

    if(Response.Status = 200) then
    begin
	    respObject := SO(Response.Msg);
	    Result.ErrorMessage := '';
	    Result.HasRun := respObject.AsObject.B['HasRun'];
	    Result.Domain := respObject.AsObject.S['Domain'];
	    Result.Step := respObject.AsObject.S['Step'];
	    Result.Substep := respObject.AsObject.S['Substep'];
	    Result.Error := respObject.AsObject.S['Error'];
	    Result.Warnings := TStringList.Create;
	    if(respObject.AsObject['Warnings'].IsType(stArray)) then
	    begin
	    	warnObject := respObject.AsObject['Warnings'].AsArray;
	    	for i := 0 to warnObject.Length - 1 do
	    		Result.Warnings.Add(warnObject.s[i]);
	    end;
    end
    else
    begin
    	Result.ErrorMessage := Response.Msg;
    end;
end;

function TAkeebaJSON.startBackup: TAkeebaResponseBackup;
var
    req		: TAkeebaRequestBackup;
begin
	Result := Self.startBackup(req);
end;

function TAkeebaJSON.stepBackup: TAkeebaResponseBackup;
var
	dataObject		: ISuperObject;
	respObject		: ISuperObject;
	warnObject		: TSuperArray;
	Response		: TAkeebaResponse;
	aProfile		: TAkeebaProfileRecord;
	i				: Integer;
begin
    dataObject := SO('{}');
    Response := runMethod('stepBackup', dataObject);

    Result.Status := Response.Status;

    if(Response.Status = 200) then
    begin
	    respObject := SO(Response.Msg);
	    Result.ErrorMessage := '';
	    Result.HasRun := respObject.AsObject.B['HasRun'];
	    Result.Domain := respObject.AsObject.S['Domain'];
	    Result.Step := respObject.AsObject.S['Step'];
	    Result.Substep := respObject.AsObject.S['Substep'];
	    Result.Error := respObject.AsObject.S['Error'];
	    Result.Warnings := TStringList.Create;
	    if(respObject.AsObject['Warnings'].IsType(stArray)) then
	    begin
	    	warnObject := respObject.AsObject['Warnings'].AsArray;
	    	for i := 0 to warnObject.Length - 1 do
	    		Result.Warnings.Add(warnObject.s[i]);
	    end;
    end
    else
    begin
    	Result.ErrorMessage := Response.Msg;
    end;
end;

function TAkeebaJSON.StrToDateTime(s: string): TDateTime;
var
    iYear, iMonth, iDay,
	iSpace		: Integer;
    sDate,
    sTime		: String;
begin
	Result := 0;
    if(s = '') then Exit;

    iSpace := Pos(' ',s);
    if(iSpace = 0) then
    begin
        sDate := Trim(s);
        sTime := '';
    end
    else
    begin
        sDate := LeftStr(s, iSpace - 1);
        sTime := MidStr(s, iSpace + 1, Length(s) - iSpace );
    end;

    if not(sDate = '') then
    begin
        iYear := StrToInt(LeftStr(sDate, Pos('-',sDate) - 1));
        sDate := MidStr( sDate, Pos('-',s)+1, Length(sDate));
        iMonth := StrToInt(LeftStr(sDate, Pos('-',sDate) - 1));
        sDate := MidStr( sDate, Pos('-',sDate)+1, Length(sDate));
        iDay := StrToInt(sDate);
        Result := Result + EncodeDate(iYear, iMonth, iDay);
    end;

    if not(sTime = '') then
    	Result := Result + StrToTime(sTime);
end;

end.

